// <copyright file="RequestSecurityTokenResponse.cs" company="SharpSTS">
// Copyright (c) 2007, 2008 All Right Reserved, http://sharpsts.com/
//
// This source is subject to the Microsoft Permissive License.
// Please see the License.txt file for more information.
// All other rights reserved.
//
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// </copyright>
// <author>Barry Dorrans</author>
// <email>barryd@idunno.org</email>
// <date>2008-06-16</date>
// <summary>Generates the RSTR.</summary>

namespace SharpSTS
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Configuration;    
    using System.IdentityModel.Tokens;
    using System.IdentityModel.Claims;
    using System.IdentityModel.Selectors;
    using System.Globalization;
    using System.Reflection;
    using System.Security.Cryptography.X509Certificates;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.Xml;

    using Configuration;
    using Properties;
    using Providers;
    using RST = RequestSecurityToken;

    /// <summary>
    /// Processes a <see cref="RequestSecurityToken" /> and builds a suitable response.
    /// </summary>
    internal sealed class RequestSecurityTokenResponse : BodyWriter
    {
        /// <summary>
        /// The security token request.
        /// </summary>
        private readonly RST rst;

        /// <summary>
        /// The certificate used to sign the response.
        /// </summary>
        private readonly X509Certificate2 signingCertificate;
        
        /// <summary>
        /// The issuer to embed in the response.
        /// </summary>
        private readonly string tokenIssuer;

        /// <summary>
        /// The current user identifier.
        /// </summary>
        private readonly object userId;

        /// <summary>
        /// A list of token serialisers.
        /// </summary>
        private static readonly Dictionary<string, TokenClassDetails> tokenSerialisers = new Dictionary<string, TokenClassDetails>();
        
        /// <summary>
        /// The claim to use as an identity claim.
        /// </summary>
        private static readonly IdentityClaim identityClaim;

        /// <summary>
        /// The object used for thread safe syncronisation.
        /// </summary>
        private static readonly object synclock = new object();

        /// <summary>
        /// Initializes the <see cref="RequestSecurityTokenResponse"/> class.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Performance", 
            "CA1810:InitializeReferenceTypeStaticFieldsInline",
            Justification = "Serialisers come from the application settings file and cannot be declared inline.")]
        static RequestSecurityTokenResponse()
        {
            // Build the token serialiser details.
            foreach (Token tokenSerialiser in STSSettings.Settings.Tokens)
            {
                int commaLocation = tokenSerialiser.SerializerType.IndexOf(",", StringComparison.OrdinalIgnoreCase);
                string className = tokenSerialiser.SerializerType.Substring(0, commaLocation);
                string assemblyName = tokenSerialiser.SerializerType.Substring(commaLocation + 1);
                Assembly assembly = Assembly.Load(assemblyName);
                tokenSerialisers.Add(tokenSerialiser.Uri, new TokenClassDetails(assembly, className));
            }

            // Check for any identity claim in the configuration file and save the information away if it exists.
            foreach (SupportedClaim supportedClaim in STSSettings.Settings.SupportedClaims)
            {
                if (!supportedClaim.IdentityClaim)
                {
                    continue;
                }

                if (null == identityClaim)
                {
                    lock (synclock)
                    {
                        if (null == identityClaim)
                        {
                            identityClaim = new IdentityClaim();
                        }
                    }
                }

                lock (synclock)
                {
                    identityClaim.Claim = supportedClaim.Uri;
                    identityClaim.NameFormat = SamlSubjectIdentifiers.Identifiers.ContainsKey(supportedClaim.IdentifierType) ? 
                                                SamlSubjectIdentifiers.Identifiers[supportedClaim.IdentifierType] : 
                                                SamlSubjectIdentifiers.Identifiers[SamlNameIdentifier.Unspecified];
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RequestSecurityTokenResponse"/> class.
        /// </summary>
        /// <param name="rst">The security token request.</param>
        /// <param name="userId">The user identifier for the current response.</param>
        public RequestSecurityTokenResponse(RST rst, object userId)
            : base(true)
        {
            this.rst = rst;
            this.userId = userId;

            string thumbprint = STSSettings.Settings.CertificateThumbprint;

            X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            store.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true);
            if (collection.Count != 1)
            {
                throw new NotSupportedException(Resources.ErrorMultipleCertificates);
            }

            this.signingCertificate = collection[0];

            this.tokenIssuer = STSSettings.Settings.Issuer.ToString();
        }

        /// <summary>
        /// Writes a valid security token response for Identity Cards.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlDictionaryWriter"></see> used to write out the message body.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Maintainability",
            "CA1506:AvoidExcessiveClassCoupling",
            Justification = "The classes are static container classes which seperate constants into their XML namespaces.")]
        protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
        {
            // Build the contents of the SAML token

            // Attributes, statements, conditions, and assertions
            List<SamlStatement> statements = new List<SamlStatement>();
            List<SamlAttribute> attributes = this.GetTokenAttributes();

            SamlSubject subject  = this.CreateSAMLSubject(attributes);

            statements.Add(new SamlAuthenticationStatement(subject, Constants.Saml.AuthenticationMethods.Unspecified, DateTime.Now, null, null, null));
            statements.Add(new SamlAttributeStatement(subject, attributes));
            SamlConditions conditions = new SamlConditions(DateTime.Now, (DateTime.Now + STSSettings.Settings.TokenValidity));
            SamlAssertion assertion = new SamlAssertion(
                string.Format(CultureInfo.InvariantCulture, Resources.AssertionFormatString, Guid.NewGuid()),
                this.tokenIssuer,
                DateTime.Now,
                conditions,
                null,
                statements);

            // Build the signing token
            SecurityToken signingToken = new X509SecurityToken(this.signingCertificate);
            SecurityKeyIdentifier keyIdentifier = new SecurityKeyIdentifier(signingToken.CreateKeyIdentifierClause<X509RawDataKeyIdentifierClause>());
            SigningCredentials signingCredentials = new SigningCredentials(
                signingToken.SecurityKeys[0],
                SecurityAlgorithms.RsaSha1Signature,
                SecurityAlgorithms.Sha1Digest,
                keyIdentifier);
            assertion.SigningCredentials = signingCredentials;

            // Build the SAML token
            SamlSecurityToken token = new SamlSecurityToken(assertion);
            SecurityKeyIdentifierClause attachedReference = token.CreateKeyIdentifierClause<SamlAssertionKeyIdentifierClause>();
            SecurityKeyIdentifierClause unattachedReference = token.CreateKeyIdentifierClause<SamlAssertionKeyIdentifierClause>();

            // Write the XML

            // RSTR
            writer.WriteStartElement(
                Constants.WSTrust.Namespace.Prefix, 
                Constants.WSTrust.Elements.RequestSecurityTokenResponse, 
                Constants.WSTrust.Namespace.Uri);
            if (this.rst.Context != null)
            {
                writer.WriteAttributeString(Constants.WSTrust.Attributes.Context, this.rst.Context);
            }

            // TokenType
            writer.WriteElementString(
                Constants.WSTrust.Namespace.Prefix, 
                Constants.WSTrust.Elements.TokenType, 
                Constants.WSTrust.Namespace.Uri, 
                Constants.WSTrust.TokenTypes.Saml10Assertion);

            // Create the serializer for the token type requested.
            if (!tokenSerialisers.ContainsKey(this.rst.TokenType))
            {
                throw new ConfigurationErrorsException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Resources.ErrorUnknownTokenType,
                        this.rst.TokenType));
            }

            TokenClassDetails details = tokenSerialisers[this.rst.TokenType];
            SecurityTokenSerializer tokenSerializer = (SecurityTokenSerializer)(details.Assembly.CreateInstance(details.ClassName, true));
            if (null == tokenSerializer)
            {
                throw new ConfigurationErrorsException(
                    string.Format(
                        CultureInfo.InvariantCulture,
                        Resources.ErrorCannotCreateSecurityTokenSerializer,
                        details.ClassName));
            }

            // Check if we have the required information to encrypt the token.
            if (null != this.rst.RelyingPartyEndpoint && this.rst.RelyingPartyEndpoint.Uri.Scheme.ToUpperInvariant() == "HTTPS")
            {
                X509CertificateEndpointIdentity x509Identity = (X509CertificateEndpointIdentity)this.rst.RelyingPartyEndpoint.Identity;
                X509Certificate2 endpointCert = x509Identity.Certificates[0];
                tokenSerializer = new XmlEncryptedTokenSerializer(endpointCert);
            }

            writer.WriteStartElement(Constants.WSTrust.Namespace.Prefix, Constants.WSTrust.Elements.RequestedSecurityToken, Constants.WSTrust.Namespace.Uri);
            tokenSerializer.WriteToken(writer, token);
            writer.WriteEndElement();

            // Display Token (if requested)
            if (this.rst.DisplayTokenRequested)
            {
                AddDisplayToken(writer, attributes);
            }

            // RequestedAttachedReference
            writer.WriteStartElement(Constants.WSTrust.Namespace.Prefix, Constants.WSTrust.Elements.RequestedAttachedReference, Constants.WSTrust.Namespace.Uri);
            tokenSerializer.WriteKeyIdentifierClause(writer, attachedReference);
            writer.WriteEndElement();

            // RequestedUnattachedReference
            writer.WriteStartElement(Constants.WSTrust.Namespace.Prefix, Constants.WSTrust.Elements.RequestedUnattachedReference, Constants.WSTrust.Namespace.Uri);
            tokenSerializer.WriteKeyIdentifierClause(writer, unattachedReference);
            writer.WriteEndElement();

            // RSTR End
            writer.WriteEndElement();
        }

        /// <summary>
        /// Adds the display token.
        /// </summary>
        /// <param name="writer">The writer.</param>
        /// <param name="tokens">The list of already populated SAML atrributes</param>
        private static void AddDisplayToken(XmlWriter writer, IEnumerable<SamlAttribute> tokens)
        {
            writer.WriteStartElement(
                Constants.WSIdentity.Namespace.Prefix,
                Constants.WSIdentity.Elements.RequestedDisplayToken,
                Constants.WSIdentity.Namespace.Uri);
            writer.WriteStartElement(
                Constants.WSIdentity.Namespace.Prefix,
                Constants.WSIdentity.Elements.DisplayToken,
                Constants.WSIdentity.Namespace.Uri);

            foreach (SamlAttribute attribute in tokens)
            {
                if (attribute.AttributeValues.Count <= 0)
                {
                    continue;
                }

                string claimType;
                if (attribute.Namespace.IndexOf("http://", StringComparison.OrdinalIgnoreCase) == 0 ||
                    attribute.Namespace.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    claimType = string.Format(
                        CultureInfo.InvariantCulture,
                        "{0}/{1}",
                        attribute.Namespace,
                        attribute.Name);
                }
                else if (attribute.Namespace.IndexOf("urn:", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    claimType = string.Format(
                        CultureInfo.InvariantCulture,
                        "{0}:{1}",
                        attribute.Namespace,
                        attribute.Name);
                }
                else
                {
                    throw new XmlException(Resources.ErrorUnknownClaimNamespaceScheme);
                }

                string displayName;
                string value;

                if (claimType == ClaimTypes.PPID)
                {
                    displayName = Resources.FriendlyNamePPID;
                    value = PPIDGeneratorProviderManager.GetFriendlyIdentifier(attribute.AttributeValues[0]);
                }
                else
                {
                    displayName = AuthorisationPolicyManager.GetDisplayName(claimType);
                    value = attribute.AttributeValues[0];
                }

                if (string.IsNullOrEmpty(displayName))
                {
                    continue;
                }

                writer.WriteStartElement(
                    Constants.WSIdentity.Namespace.Prefix,
                    Constants.WSIdentity.Elements.DisplayClaim,
                    Constants.WSIdentity.Namespace.Uri);
                writer.WriteAttributeString(Constants.WSIdentity.Attributes.Uri, claimType);

                writer.WriteStartElement(
                    Constants.WSIdentity.Namespace.Prefix,
                    Constants.WSIdentity.Elements.DisplayTag,
                    Constants.WSIdentity.Namespace.Uri);
                writer.WriteString(displayName);
                writer.WriteEndElement();

                writer.WriteStartElement(
                    Constants.WSIdentity.Namespace.Prefix,
                    Constants.WSIdentity.Elements.Description,
                    Constants.WSIdentity.Namespace.Uri);
                writer.WriteString(claimType);
                writer.WriteEndElement();

                writer.WriteStartElement(
                    Constants.WSIdentity.Namespace.Prefix,
                    Constants.WSIdentity.Elements.DisplayValue,
                    Constants.WSIdentity.Namespace.Uri);
                writer.WriteString(value);
                writer.WriteEndElement();

                writer.WriteEndElement();
            }

            writer.WriteEndElement();
            writer.WriteEndElement();
        }

        /// <summary>
        /// Creates the SAML subject.
        /// </summary>
        /// <param name="tokenAttributes">The token attributes for the subject.</param>
        /// <returns>A SAML subject for the RSTR.</returns>
        private SamlSubject CreateSAMLSubject(IEnumerable<SamlAttribute> tokenAttributes)
        {
            SamlSubject subject = new SamlSubject();
            if (this.rst.UseKey != null)
            {
                // Add the key and the Holder-Of-Key confirmation method
                subject.KeyIdentifier = this.rst.UseKey;
                subject.ConfirmationMethods.Add(SamlConstants.HolderOfKey);
            }
            else
            {
                // This is a bearer token
                subject.ConfirmationMethods.Add(SamlConstants.SenderVouches);
            }

            // Add the SAML subject if we have an identity claim configured
            if (identityClaim != null && this.rst.RequestedClaims.Contains(identityClaim.Claim))
            {
                subject.NameFormat = identityClaim.NameFormat;
                foreach (SamlAttribute attribute in tokenAttributes)
                {
                    string claimType = null;
                    if (attribute.Namespace.IndexOf("http://", StringComparison.OrdinalIgnoreCase) == 0 ||
                        attribute.Namespace.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        claimType = string.Format(
                            CultureInfo.InvariantCulture,
                            "{0}/{1}",
                            attribute.Namespace,
                            attribute.Name);
                    }
                    else if (attribute.Namespace.IndexOf("urn:", StringComparison.OrdinalIgnoreCase) == 0)
                    {
                        claimType = string.Format(
                            CultureInfo.InvariantCulture,
                            "{0}:{1}",
                            attribute.Namespace,
                            attribute.Name);
                    }

                    if (claimType == identityClaim.Claim)
                    {
                        subject.Name = attribute.AttributeValues[0];
                    }
                }
            }

            return subject;
        }

        /// <summary>
        /// Returns the claims requested by the <see cref="RequestSecurityToken"/>.
        /// </summary>
        /// <returns>A list of SamlAttributes and their resources.</returns>
        private List<SamlAttribute> GetTokenAttributes()
        {
            Claim ppid = null;

            List<SamlAttribute> result = new List<SamlAttribute>();

            List<string> requestedClaims = new List<string>(this.rst.RequestedClaims);
            
            // Remove the PPID claim if it exists; we need to call the PPID provider instead.
            if (requestedClaims.Contains(ClaimTypes.PPID))
            {
                requestedClaims.Remove(ClaimTypes.PPID);
                ppid = new Claim(
                    ClaimTypes.PPID, 
                    PPIDGeneratorProviderManager.GetPPID(this.rst.CardId, this.rst.RelyingPartyEndpoint, this.rst.PPID),
                    Rights.PossessProperty);
            }

            Collection<Claim> filledClaims = requestedClaims.Count > 0 ? AuthorisationPolicyManager.GetClaims(
                                                                                                       this.userId, 
                                                                                                       this.rst.RelyingPartyEndpoint, 
                                                                                                       requestedClaims.AsReadOnly()) : 
                                                                         new Collection<Claim>();
            
            // If we have a PPID then add it back to the filled claims list.
            if (null != ppid)
            {
                filledClaims.Add(ppid);
            }

            foreach (Claim claim in filledClaims)
            {
                SamlAttribute samlAttribute = null;
                if (claim.ClaimType.IndexOf("http://", StringComparison.OrdinalIgnoreCase) == 0 ||
                    claim.ClaimType.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    samlAttribute = new SamlAttribute(claim);
                }
                else if (claim.ClaimType.IndexOf("urn:", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    string claimNamespace = claim.ClaimType.Substring(0, claim.ClaimType.LastIndexOf(":", StringComparison.OrdinalIgnoreCase));
                    string claimName = claim.ClaimType.Substring(claim.ClaimType.LastIndexOf(":", StringComparison.OrdinalIgnoreCase) + 1);
                    samlAttribute = new SamlAttribute(claimNamespace, claimName, new[] { claim.Resource.ToString() });
                }

                if (null != samlAttribute)
                {
                    result.Add(samlAttribute);
                }
            }

            return result;
        }

        #region Helper Classes
        /// <summary>
        /// Represents the details necessary to create a token serialiser.
        /// </summary>
        private class TokenClassDetails
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="TokenClassDetails"/> class.
            /// </summary>
            /// <param name="assembly">The assembly.</param>
            /// <param name="className">Name of the class.</param>
            public TokenClassDetails(Assembly assembly, string className)
            {
                this.Assembly = assembly;
                this.ClassName = className;
            }

            /// <summary>
            /// Gets or sets the assembly containing the serialisation class.
            /// </summary>
            /// <value>The assembly containing the serialisation class.</value>
            public Assembly Assembly
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the name of the serialisation class.
            /// </summary>
            /// <value>The name of the serialisation class.</value>
            public string ClassName
            {
                get;
                set;
            }
        }

        /// <summary>
        /// Represents the information necessary to provide an identity claim.
        /// </summary>
        private class IdentityClaim
        {
            /// <summary>
            /// Gets or sets the claim URI.
            /// </summary>
            public string Claim
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the type/format of the identity claim.
            /// </summary>
            public string NameFormat
            {
                get;
                set;
            }
        }
        #endregion
    }
}